package com.ifp.opengate.boot.filter;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;

import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ifp.adapter.AdapterHandle;
import com.ifp.adapter.common.AdapterCanstants;
import com.ifp.adapter.exception.AdapterException;
import com.ifp.core.context.ClogicContext;
import com.ifp.core.exception.BaseException;
import com.ifp.core.log.Trace;
import com.ifp.core.util.DateUtil;
import com.ifp.core.util.SpringContextsUtil;
import com.ifp.core.util.StringUtil;
import com.ifp.opengate.boot.bean.OpenGateRoute;
import com.ifp.opengate.boot.constants.Constants;
import com.ifp.opengate.boot.dao.ApiLogMapper;
import com.ifp.opengate.boot.emum.ErrorEnum;
import com.ifp.opengate.boot.entity.ApiLog;
import com.ifp.opengate.boot.exception.AccessControlException;
import com.ifp.opengate.boot.task.ConnectorCacheDataTask;
import com.ifp.opengate.boot.utils.RedisUtils;
import com.ifp.opengate.boot.utils.RequestUtil;
import com.ifp.web.common.IFPConstance;

import io.netty.buffer.ByteBufAllocator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * Created by mogs on 2019/4/18. 参数初始化filter，用于解析请求的参数,并创建IFP中的CLContext等初始化请求操作
 */
@Component
public class RequestInitFilter implements GlobalFilter, Ordered {
    /**
     * 默认报文格式
     */
    private String defaultType = "J";

    @Autowired
    private RedisUtils redisUtils;

    @Autowired
    private ApiLogMapper apiLogMapper;

    /**
     * 重新加载数据到缓存的请求URL
     */
    private static final String RELOAD_CACHE_DATA_URL = "/reload/gateway.reloadCacheData";

    /**
     * 请求获取Token 的路径
     */
    private static final String GET_TOKEN_URL = "/query/gateway.getToken";

    /**
     * 上传文件至 OSS 服务接口地址
     */
    private static final String OSS_UPLOAD_URL = "/upload/contract.oss.upload";

    /**
     * 上传文件并同时调用 e 签宝无认证 PDF 文件签署 URL 地址
     */
    private static final String UPLOAD_FILE_AND_SIGN_WITHOUT_AUTH_URL =
        "/upload/contract.sign.uploadFileAndSignWithoutAuth";

    @Autowired
    private ConnectorCacheDataTask connectorCacheDataTask;

    @Override
    public Mono<Void> filter(ServerWebExchange serverWebExchange, GatewayFilterChain gatewayFilterChain) {
        ServerHttpRequest processedRequest = serverWebExchange.getRequest();
        String actionId = RequestUtil.getActionId(processedRequest);
        Route route = serverWebExchange.getAttribute(GATEWAY_ROUTE_ATTR);
        OpenGateRoute openGateRoute = connectorCacheDataTask.getRouteOfMap().get(route.getId());
        // Trace.log(Trace.MODULE_TRACE,Trace.LEVEL_ERROR,"Thead id is :{}",Thread.currentThread().getId());
        // 此日志必须放在前面，确保一打印日志就具有链路ID
        // Trace.log(Trace.MODULE_MVC, Trace.LEVEL_INFO, "start request: {}", actionId);
        ClogicContext clogicContext = new ClogicContext();
        clogicContext.setCreateTime(DateUtil.getStringToday());
        String requestType = this.setRequestType(processedRequest, clogicContext);
        clogicContext.setTransCode(actionId);
        Map<String, Object> requestDataMap = new ConcurrentHashMap<>();
        /**
         * 报文转换，根据不同报文类型（如K-V格式、JSON格式、XML格式、SOAP）
         */
        try {
            if (requestType.endsWith(defaultType)) {
                Object bodyObject = serverWebExchange.getAttribute("cachedRequestBodyObject");
                if (bodyObject == null) {
                    Flux<DataBuffer> body = processedRequest.getBody();

                    StringBuilder sb = new StringBuilder();
                    body.subscribe(buffer -> {
                        byte[] bytes = new byte[buffer.readableByteCount()];
                        buffer.read(bytes);
                        DataBufferUtils.release(buffer);
                        String bodyString = new String(bytes, StandardCharsets.UTF_8);
                        sb.append(bodyString);
                    });

                    String bodyStr = sb.toString();
                    DataBuffer bodyDataBuffer = stringBuffer(bodyStr);
                    Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);

                    processedRequest = new ServerHttpRequestDecorator(processedRequest) {
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return bodyFlux;
                        }
                    };
                    if (StringUtils.isNotEmpty(bodyStr)) {
                        requestDataMap = ((AdapterHandle)SpringContextsUtil.getBean("adapterHandle"))
                            .unformat(requestType, bodyStr, null);
                        Object fileBase64Temp;
                        if (UPLOAD_FILE_AND_SIGN_WITHOUT_AUTH_URL.equals(actionId) || OSS_UPLOAD_URL.equals(actionId)) {
                            if (requestDataMap != null && !requestDataMap.isEmpty()) {
                                // Map<String, Object> tempMap = new HashMap<>();
                                // tempMap.putAll(requestDataMap);
                                Object serviceBody = requestDataMap.get("body");
                                if (serviceBody instanceof Map) {
                                    Map<String, Object> serviceBodyMap = (Map<String, Object>)serviceBody;
                                    /*
                                     注意：目前中台涉及的文件流接口的请求字段，参数名均为 fileBase64
                                     */
                                    // 临时删除该 key，打印日志不打印该打字段
                                    fileBase64Temp = serviceBodyMap.remove("fileBase64");
                                    Trace.log(Trace.MODULE_DATA, 1,
                                        "receive msg by JSONProcessorNoLog and remove fileBase64 :{}", requestDataMap);
                                    // 打印完日志再恢复原报文
                                    serviceBodyMap.put("fileBase64", fileBase64Temp);
                                }
                            }
                        } else {
                            Trace.log(Trace.MODULE_DATA, 1, "receive msg by JSONProcessorNoLog  :{}", requestDataMap);
                        }
                    }
                    DataBufferUtils.release(bodyDataBuffer);
                } else {
                    requestDataMap = (Map)bodyObject;
                }
            } else {
                requestDataMap.putAll(processedRequest.getQueryParams().toSingleValueMap());
            }
            // Trace.log(Trace.MODULE_FILTER, Trace.LEVEL_INFO, "requestDataMap's data: {}", requestDataMap);
            this.setCLogicDataMap(clogicContext, requestDataMap);
            clogicContext.setMonitorId((String)serverWebExchange.getAttributes().get("logId"));
            serverWebExchange.getAttributes().put(Constants.CL_CONTEXT, clogicContext);
            // 校验报文头
            // this.checkRequsetHeader(serverWebExchange);
        } catch (AdapterException e) {
            // Trace.logError(Trace.MODULE_FILTER,"报文转换异常：",e);
            throw e;
        } catch (BaseException e) {
            // Trace.logError(Trace.MODULE_FILTER,"报文转换异常：",e);
            throw e;
        }
        loadRedisInitData(processedRequest, clogicContext);
        LambdaQueryWrapper<ApiLog> queryWrapperApiLog = new LambdaQueryWrapper();
        queryWrapperApiLog.eq(ApiLog::getRequestId, requestDataMap.get("requestId"));
        if (apiLogMapper.selectCount(queryWrapperApiLog) > 0) {
            throw new BaseException(ErrorEnum.requestIdRepeadError.code(), ErrorEnum.requestIdRepeadError.msg());
        }
        // 若是请求Token请求和重新加载动态路由请求，不记录日志
        if (!GET_TOKEN_URL.equals(actionId) && !RELOAD_CACHE_DATA_URL.equals(actionId)) {
            ApiLog apiLog = new ApiLog();
            if (requestDataMap.get("channel") != null) {
                apiLog.setChannel(requestDataMap.get("channel").toString());
            }
            if (requestDataMap.get("requestId") != null) {
                apiLog.setRequestId(requestDataMap.get("requestId").toString());
            }
            if (requestDataMap.get("appId") != null) {
                apiLog.setAppId(requestDataMap.get("appId").toString());
            }
            if (requestDataMap.get("appName") != null) {
                apiLog.setAppName(requestDataMap.get("appName").toString());
            }
            if (requestDataMap.get("userId") != null && !StringUtils.isEmpty(requestDataMap.get("userId").toString())) {
                apiLog.setUserId(Integer.valueOf(requestDataMap.get("userId").toString()));
            }
            apiLog.setRouteId(openGateRoute.getRouteId());
            apiLog.setRouteName(openGateRoute.getRouteName());
            apiLog.setPath(openGateRoute.getRoutePath());
            if (requestDataMap.get("clientIp") != null) {
                apiLog.setClientIp(requestDataMap.get("clientIp").toString());
            }
            apiLog.setClientIp(getIpAddress(processedRequest));
            apiLog.setRequestTime(LocalDateTime.now());
            apiLog.setGmtCreate(LocalDateTime.now());
            apiLog.setIsSuccess(0);
            apiLogMapper.insert(apiLog);
        }
        return gatewayFilterChain.filter(serverWebExchange.mutate().request(processedRequest).build())
            .then(Mono.fromRunnable(() -> {
                // Trace.log(Trace.MODULE_TRACE,Trace.LEVEL_ERROR,"RequestInitFilter resp Thead id is
                // :{}",Thread.currentThread().getId());
            }));
    }

    /**
     * 获取当前请求IP
     *
     * @param request
     * @return
     */
    private String getIpAddress(ServerHttpRequest request) {
        String ip = RequestUtil.getHeaderValue(request, "X-Forwarded-For");
        Trace.log("FILTER", 1, "getIpAddress(HttpServletRequest) - X-Forwarded-For - String ip={}", new Object[] {ip});
        if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
            Trace.log("FILTER", 1, "代理IP:{}", new Object[] {ip});
            if (ip.length() > 15) {
                String[] ips = ip.split(",");

                for (int index = 0; index < ips.length; ++index) {
                    String strIp = ips[index];
                    if (!"unknown".equalsIgnoreCase(strIp)) {
                        ip = strIp;
                        break;
                    }
                }
            }
        } else {
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = RequestUtil.getHeaderValue(request, "Proxy-Client-IP");
                Trace.log("FILTER", 0, "1.getIpAddress(HttpServletRequest) - Proxy-Client-IP - String ip={}",
                    new Object[] {ip});
            }

            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = RequestUtil.getHeaderValue(request, "WL-Proxy-Client-IP");
                Trace.log("FILTER", 0, "2.getIpAddress(HttpServletRequest) - WL-Proxy-Client-IP - String ip={}",
                    new Object[] {ip});
            }

            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = RequestUtil.getHeaderValue(request, "HTTP_CLIENT_IP");
                Trace.log("FILTER", 0, "3.getIpAddress(HttpServletRequest) - HTTP_CLIENT_IP - String ip={}",
                    new Object[] {ip});
            }

            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = RequestUtil.getHeaderValue(request, "HTTP_X_FORWARDED_FOR");
                Trace.log("FILTER", 0, "4.getIpAddress(HttpServletRequest) - HTTP_X_FORWARDED_FOR - String ip={}",
                    new Object[] {ip});
            }

            if (request.getRemoteAddress() != null) {
                ip = request.getRemoteAddress().getHostString();
                Trace.log("FILTER", 0, "5.getRemoteAddress - String ip={}", new Object[] {ip});
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getURI().getHost();
                Trace.log("FILTER", 0, "6.getIpAddress(HttpServletRequest) - getRemoteAddr - String ip={}",
                    new Object[] {ip});
            }
        }

        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }

    /**
     * 加载redis初始数据
     *
     * @param request
     * @param clogicContext
     */
    private void loadRedisInitData(ServerHttpRequest request, ClogicContext clogicContext) {
        try {
            // 获取联调测试报告接口
            String contextPath = "/open-gate";
            String reqStr = (String)redisUtils.get("opengate_apiWhite_qryCaseReportApi");
            if (StringUtils.isNotEmpty(reqStr)) {
                if (reqStr.contains(contextPath)) {
                    reqStr = reqStr.substring(reqStr.indexOf(contextPath) + contextPath.length());
                }
                int idx = reqStr.lastIndexOf(".");

                if (idx != -1) {
                    reqStr = reqStr.substring(0, idx);
                }
                clogicContext.getTempMap().put(Constants.GW_TEMP_KEY + "qryCaseReportApiCode", reqStr);
            }
            // 是否走沙箱
            // TODO

            // 获取当前SDK版本号
            String sdkVersion = (String)redisUtils.get("opengate_sdk_version");
            clogicContext.getTempMap().put(Constants.GW_TEMP_KEY + "sdkVersion", sdkVersion);
        } catch (Exception e) {
            Trace.logError(Trace.MODULE_COMMON, "加载redis数据出错", e);
        }
    }

    /**
     * 字符串数据转换为DataBuffer
     *
     * @param value
     * @return
     */
    protected DataBuffer stringBuffer(String value) {
        byte[] bytes = value.getBytes(StandardCharsets.UTF_8);

        NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
        DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
        buffer.write(bytes);
        return buffer;
    }

    /**
     * 请求参数保存至ClogicContext
     *
     * @param clogicContext
     * @param requestDataMap
     * @throws AdapterException
     */
    private void setCLogicDataMap(ClogicContext clogicContext, Map<String, Object> requestDataMap)
        throws AdapterException {
        if (null == requestDataMap) {
            throw new AdapterException("请求参数集合为空！");
        }

        // 检查是否存在header
        if (requestDataMap.get(AdapterCanstants.HEADER) == null) {
            // throw new AdapterException("请求参数异常，header为空！");
        }

        // 检查是否存在body
        if (requestDataMap.get(AdapterCanstants.BODY) == null) {
            // throw new AdapterException("请求参数异常，body为空！");
        }
        Object headerMap = requestDataMap.get(AdapterCanstants.HEADER);
        if (!StringUtil.hasText(headerMap)) {
            headerMap = new HashMap<>();
        }
        Object bodyMap = requestDataMap.get(AdapterCanstants.BODY);
        if (!StringUtil.hasText(bodyMap)) {
            bodyMap = new HashMap<>();
        }
        if (headerMap instanceof Map) {
            requestDataMap.putAll((Map<String, Object>)headerMap);
        }
        /*if (bodyMap instanceof Map) {
            requestDataMap.putAll((Map<String, Object>)bodyMap);
        }*/
        requestDataMap.put(IFPConstance.MVC_TYPE, clogicContext.getRequestType());
        // requestDataMap.put(AdapterCanstants.HEADER, (requestDataMap.get(AdapterCanstants.HEADER) instanceof
        // Map)?(Map<String,Object>)requestDataMap.get(AdapterCanstants.HEADER):new HashMap());
        clogicContext.setDataMap(requestDataMap);
    }

    /**
     * 设置requestType
     *
     * @param request
     * @param clogicContext
     */
    public String setRequestType(ServerHttpRequest request, ClogicContext clogicContext) {
        String requestType = defaultType;
        if (request.getHeaders().containsKey(IFPConstance.MVC_TYPE)) {
            requestType = request.getHeaders().getFirst(IFPConstance.MVC_TYPE);
        }
        if (request.getQueryParams().containsKey(IFPConstance.MVC_TYPE)) {
            requestType = request.getQueryParams().getFirst(IFPConstance.MVC_TYPE);
        }
        clogicContext.setRequestType(requestType);
        return requestType;
    }

    /**
     * 校验报文头
     *
     * @param exchange 上下文
     */
    private void checkRequsetHeader(ServerWebExchange exchange) {
        ClogicContext clogicContext = (ClogicContext)exchange.getAttributes().get(Constants.CL_CONTEXT);
        Map<String, Object> dataMap = clogicContext.getDataMap();
        Map<String, Object> headerMap = (Map<String, Object>)dataMap.get(AdapterCanstants.HEADER);
        // 报文头必输、长度校验
        this.checkForceSendFiled(headerMap);
        this.checkKeySendFiled(headerMap);
        this.checkchoseSendFiled(headerMap);
        this.checkSpecileSendFiled(headerMap);
    }

    /**
     * 校验键值必填参数
     *
     * @param headerMap 请求报文头参数
     */
    @SuppressWarnings("unchecked")
    private void checkForceSendFiled(Map<String, Object> headerMap) {
        // 键值必须上送的字段
        Map<String, Integer> forceSendMap = (Map<String, Integer>)SpringContextsUtil.getBean("checkHeaderForceSendMap");
        for (Entry<String, Integer> entry : forceSendMap.entrySet()) {
            if (StringUtils.isBlank((String)headerMap.get(entry.getKey()))) {
                // Trace.logError(Trace.MODULE_VALIDATION, "{}为空", entry.getKey());
                throw new AccessControlException(ErrorEnum.paramError.code(),
                    String.format(ErrorEnum.paramError.msg(), entry.getKey()));
            }
            // 长度校验
            if (((String)headerMap.get(entry.getKey())).length() > entry.getValue()) {
                // Trace.logError(Trace.MODULE_VALIDATION, "{}太长", entry.getKey());
                throw new AccessControlException(ErrorEnum.paramValueError.code(),
                    String.format(ErrorEnum.paramValueError.msg(), entry.getKey()));
            }
        }
    }

    /**
     * 校验键必填参数
     *
     * @param headerMap 请求报文头参数
     */
    @SuppressWarnings("unchecked")
    private void checkKeySendFiled(Map<String, Object> headerMap) {
        // 键必须上送的字段
        Map<String, Integer> keySendMap = (Map<String, Integer>)SpringContextsUtil.getBean("checkHeaderKeySendMap");
        for (Entry<String, Integer> entry : keySendMap.entrySet()) {
            if (!headerMap.containsKey(entry.getKey())) {
                // Trace.logError(Trace.MODULE_VALIDATION, "{}为空", entry.getKey());
                throw new AccessControlException(ErrorEnum.paramError.code(),
                    String.format(ErrorEnum.paramError.msg(), entry.getKey()));
            }
            // 长度校验
            if (StringUtils.isBlank((String)headerMap.get(entry.getKey()))) {
                continue;
            }
            if (((String)headerMap.get(entry.getKey())).length() > entry.getValue()) {
                // Trace.logError(Trace.MODULE_VALIDATION, "{}太长", entry.getKey());
                throw new AccessControlException(ErrorEnum.paramValueError.code(),
                    String.format(ErrorEnum.paramValueError.msg(), entry.getKey()));
            }
        }
    }

    /**
     * 校验可选参数
     *
     * @param headerMap 请求报文头参数
     */
    @SuppressWarnings("unchecked")
    private void checkchoseSendFiled(Map<String, Object> headerMap) {
        // 可选字段
        Map<String, Integer> choseSendMap = (Map<String, Integer>)SpringContextsUtil.getBean("checkHeaderChoseSendMap");
        for (Entry<String, Integer> entry : choseSendMap.entrySet()) {
            if (StringUtils.isNotBlank((String)headerMap.get(entry.getKey()))
                && ((String)headerMap.get(entry.getKey())).length() > entry.getValue()) {
                // 长度校验
                // Trace.logError(Trace.MODULE_VALIDATION, "{}太长", entry.getKey());
                throw new AccessControlException(ErrorEnum.paramError.code(),
                    String.format(ErrorEnum.paramError.msg(), entry.getKey()));
            }
        }
    }

    /**
     * 请求报文头校验
     *
     * @param headerMap 请求报文头参数
     */
    private void checkSpecileSendFiled(Map<String, Object> headerMap) {
        // 合作方商户ID+2位序号
        String appId = (String)headerMap.get("appId");
        // yyyyMMddHHmmssSSS+appId+3位随机数
        String requestId = (String)headerMap.get("requestId");
        if (requestId.length() != (17 + appId.length() + 3)) {
            throw new AccessControlException(ErrorEnum.requestIdParamLegalityError.code(),
                ErrorEnum.requestIdParamLegalityError.msg());
        }
        if (!appId.equals(requestId.substring(17, 17 + appId.length()))) {
            throw new AccessControlException(ErrorEnum.requestIdParamLegalityError.code(),
                ErrorEnum.requestIdParamLegalityError.msg());
        }
        // 格式1.0.0
        String version = (String)headerMap.get("version");
        if (!Pattern.matches("(^\\d+\\.\\d+\\.\\d+$)", version)) {
            throw new AccessControlException(ErrorEnum.versionLegalityError.code(),
                ErrorEnum.versionLegalityError.msg());
        }
    }

    @Override
    public int getOrder() {
        return -16;
    }
}
